CloudFront 署名付きURLと署名付きCookieをおさらいしてPythonで試してみた
Guten Abend, ベルリンの伊藤です。
SAPの勉強中、Trusted Signer, OAI なんてキーワードが出てきて、結構理解が曖昧だったことに気付いたので一からおさらいしました。
ドキュメントを元に要約した各設定の解釈と、Pythonによるやってみたを載せます。
導入
S3などオリジンに配置してあるコンテンツをCloudFront経由で配信します。主な目的は配信の高速化ですね。毎度オリジンまで取りに行かなくても、世界中にあるCloudFrontエッジロケーションにより、ユーザに近いエッジから配信されます。
そしてプライベートなコンテンツを制限を設けて配信したい場合には、本記事で取り扱う 署名付き URL/Cookie を使うことができます。
これは、誰でもかれでもCloudFrontを経由したらコンテンツを入手できないよう、次の図でいう【ユーザからCloudFrontへの矢印】を制御する時に使います。また、オリジンへ直接アクセスできるままになっていては元も子もないので、オリジンへのアクセスは以下のいずれかで制御します。
- S3オリジン: Origin Access Identity (OAI) を設定 (OAIという特別なCFユーザを作成し、CFに紐づけ、S3バケットポリシーで許可)
- それ以外のオリジン: カスタムヘッダを設定する
OAI は下記ブログでも紹介されています。
署名付きURLと署名付きCookie
- 署名付き〜 (Signed~)
- URL: 個別のファイルを制限したい場合、ユーザがCookieをサポートしないクライアントを使用する場合に(ファイルごとにランダムな文字列(=署名)を含むURLを発行してそこにアクセスしてもらう)
- Cookie: 特定条件の複数ファイルを制限したい場合、現在のURLを変えたくない場合に(事前にリクエストへのレスポンスで特別なヘッダを返却しておき、その値(=署名)をヘッダに含めることでアクセスしてもらう)
- 設けられる制限
- 既定ポリシー: アクセスの有効期限
- カスタムポリシー: アクセスの有効期間、ユーザのアクセス元IP
- 設定区分
- CF ディストリビューションの Behavior (パスごとに設定分けていれば、パスごとに)
- 署名者: 署名付きURL/Cookieの作成に使う
- 信頼されたキーグループ(Trusted Key Group): キー自作・AWS推奨
- CFキーペアを持つAWSアカウント(Trusted Signer): root作業につき非推奨
署名者(signer)については英語名が少しややこしいのですが、CFコンソール表記によると、2つ目のAWSアカウントの方を"Trusted Signer"と呼ぶみたいです。
資格勉強でサンプル問題文に "OAI" と "Trusted Signer" が出てきて混同していたのですが、OAIはS3アクセス制限のために使うCF特別ユーザ、Trusted SignerはでCFキーペアを使って署名付き~を発行するAWSアカウントと覚えておき、使いどこ(冒頭図の矢印で右か左か)を押さえておけば良さそうですね。
検証
署名者の準備
上述の通り Trusted Signer はルートアカウントによる作業が必要なため、今では推奨されていません。今回も信頼されたキーグループでやっていきます。大まかな手順は以下の通りですが、ドキュメントも十分に詳しいのと、↓後述のZennの記事がすべてを物語っているので私はこれだけに留めます。
- ローカルで公開鍵と秘密鍵のキーペアを作る
- 公開鍵をCloudFrontへアップロード(*このキーペアIDをURL発行時に使います)
- CloudFrontでキーグループを作成し、アップロードした公開鍵を追加
- 対象ディストリビューション→Behaviorを編集し、作成したキーグループを指定する
署名付きURLの発行
署名付きURLのおさらい:
個別のファイルを制限したい場合、ユーザがCookieをサポートしないクライアントを使用する場合に(ファイルごとにランダムな文字列(=署名)を含むURLを発行してそこにアクセスしてもらう)
もちろん既に検証している方々がいて、とても分かりやすいですので、参考にしてください!
公式の各種言語によるサンプルはドキュメントにリンクがあります。以下はほぼPythonのサンプルの通りです。
""" pip install cryptography botocore """ from datetime import datetime, timedelta from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import padding from botocore.signers import CloudFrontSigner def rsa_signer(message): with open('./private_key.pem', 'rb') as key_file: #Specify the pem file private_key = serialization.load_pem_private_key( key_file.read(), password=None, backend=default_backend() ) return private_key.sign(message, padding.PKCS1v15(), hashes.SHA1()) key_id = 'KPSXXXXXXXXXX' #Specify the public key ID url = 'https://d1xxxxxxxxxxxx.cloudfront.net/' #Specify the distribution URL file = 'Hoya-king.png' expire_date = datetime.utcnow() + timedelta(minutes=30) #Specify the expiry date cloudfront_signer = CloudFrontSigner(key_id, rsa_signer) signed_url = cloudfront_signer.generate_presigned_url( url + file, date_less_than=expire_date) print(signed_url)
このスクリプトを実行すると、署名付きURLが返却されます。URLに含まれるエポック時間 1619021278 を確認してみると、ローカル時間で 2021-04-21 6:07:58 PM と、指定した通り30分後であることが確認できます。
21-04-21 17:37:53 % python test.py https://d1xxxxxxxxxxxx.cloudfront.net/Hoya-king.png?Expires=1619021278&Signature=19z5Hxxxxxxxx&Key-Pair-Id=KPSXXXXXXXXXX
有効期限内では、URLを開くと画像へアクセスすることができます。
なお、署名なしのURL(https://d1xxxxxxxxxxxx.cloudfront.net/Hoya-king.png)へアクセスすると MissingKey エラー、他に以下のパターンでは AccessDenied エラーとなります。
- S3オリジンURL(https://bucket-name.s3.eu-central-1.amazonaws.com/Hoya-king.png)
- 有効期限切れの署名付きURL
- 署名付きURLを発行し直した場合、以前の署名付きURL
署名付きCookie
署名付きCookieのおさらい:
特定条件の複数ファイルを制限したい場合、現在のURLを変えたくない場合に(事前にリクエストへのレスポンスで特別なヘッダを返却しておき、その値(=署名)をヘッダに含めることでアクセスしてもらう)
個人的に、URLの方はだいたい思ってた通りという感じだったんですが、Cookieの方の理解がだいぶ曖昧だったので、もう少し詳しく。ドキュメントを要約すると以下のような仕組みとなっています。
- コンテンツにアクセスするユーザは、特定の条件を満たす(ウェブサイトにサインインしてコンテンツを購入するなど)
- 条件を満たしたユーザに対し、アプリケーションからのレスポンスに
Set-Cookie
ヘッダ(名前と値のペア)を含める(購入完了時、購入完了したユーザのログイン時などで) - ユーザはブラウザ等でコンテンツにアクセスする際、受け取った名前と値ペア(=署名付きCookie)をリクエストのヘッダに含める
- リクエストを受け取ったCFは署名付きCookieが有効であることを確認し、アクセスを許可する
この Set-Cookie
ヘッダは、ポリシー(有効期限やソースIP)、署名、キーペアIDの3つが必要です。
コード
こちらも、公式の各種言語によるサンプルはドキュメントにリンクがありますが、Pythonはありませんでした。
今回はこちらのGitHubを主に参考にさせていただきました。rsa_signer
等の骨子は公式に合わせる形で置き換え、確認用 curl を出力する generate_curl_cmd
は こちらのGitHubを参考にさせていただきました。
""" pip install cryptography botocore requests """ from datetime import datetime, timedelta from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import padding from botocore.signers import CloudFrontSigner import requests def rsa_signer(message): with open('./private_key.pem', 'rb') as key_file: #Specify the pem file private_key = serialization.load_pem_private_key( key_file.read(), password=None, backend=default_backend() ) return private_key.sign(message, padding.PKCS1v15(), hashes.SHA1()) def generate_signed_cookies(cf_signer, key_id, url, expire_date): policy = cf_signer.build_policy(url, expire_date).encode('utf8') policy_64 = cf_signer._url_b64encode(policy).decode('utf8') signature = rsa_signer(policy) signature_64 = cf_signer._url_b64encode(signature).decode('utf8') return { "CloudFront-Policy": policy_64, "CloudFront-Signature": signature_64, "CloudFront-Key-Pair-Id": key_id, } def generate_curl_cmd(url, cookies): curl_cmd = "curl -D -" for k, v in cookies.items(): curl_cmd += " -H 'Cookie: {}={}'".format(k, v) curl_cmd += " -O {}".format(url) return curl_cmd key_id = 'KPSXXXXXXXXXX' #Specify the public key ID url = 'https://d1xxxxxxxxxxxx.cloudfront.net/' #Specify the distribution URL file = 'Hoya-king-2.png' expire_date = datetime.utcnow() + timedelta(minutes=1) #Specify the expiry date cf_signer = CloudFrontSigner(key_id, rsa_signer) signed_cookies = generate_signed_cookies(cf_signer, key_id, url + file, expire_date) r = requests.get(url + file, cookies=signed_cookies) print(f'Results: {r.status_code}') print(r.headers) print(generate_curl_cmd(url + file, signed_cookies)) # returns a curl command for testing
今回はCookieを用いたリクエストですので、コード内のrequests.get
で取得が可能かどうか確認します。ステータスコードが200ならばアクセス成功で、ヘッダからも結果が確認できます。
また、確認用に手動で実行する curl コマンドも出力するようにしました。
実行
スクリプトを実行してみると、200が無事に返されました!確認用に出力された curl コマンドを見てみると、 -H オプションで3つのヘッダが付与されていることがわかります。(CloudFront-PolicyとCloudFront-Signatureはだいぶ長い値が入ります)
そして、この curl コマンドをコピーしてすぐに実行してみると、これも200で成功しました。ファイルを保存する-Oオプションを付けているため、画像ファイルも取得することができました。
21-04-21 19:39:46 % python test2.py Results: 200 æ'Content-Type': 'image/png', 'Content-Length': '436473', 'Connection': 'keep-alive', 'Date': 'Wed, 21 Apr 2021 17:38:39 GMT', 'Last-Modified': 'Wed, 21 Apr 2021 15:35:58 GMT', 'ETag': '"e617e131d9c73e2671b44b4cbb587027"', 'Accept-Ranges': 'bytes', 'Server': 'AmazonS3', 'X-Cache': 'Hit from cloudfront', 'Via': '1.1 f2db75b601dc30df73b1beb29596a375.cloudfront.net (CloudFront)', 'X-Amz-Cf-Pop': 'FRA53-C1', 'X-Amz-Cf-Id': '7fISWUXYMx9gHqFjKKhJQ6vcLZhevETcs3j-uyJn_BuePc2-wb-9tQ==', 'Age': '73'å curl -D - -H 'Cookie: CloudFront-Policy=eyJxxx' -H 'Cookie: CloudFront-Signature=N6bxxx' -H 'Cookie: CloudFront-Key-Pair-Id=KPSXXXXXXXXXX' -O https://d1xxxxxxxxxxxx.cloudfront.net/Hoya-king-2.png 21-04-21 19:39:51 % curl -D (中略) -O https://d1xxxxxxxxxxxx.cloudfront.net/Hoya-king-2.png % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0HTTP/2 200 content-type: image/png content-length: 436473 date: Wed, 21 Apr 2021 17:40:01 GMT last-modified: Wed, 21 Apr 2021 15:35:58 GMT etag: "e617e131d9c73e2671b44b4cbb587027" accept-ranges: bytes server: AmazonS3 x-cache: Miss from cloudfront via: 1.1 f2db75b601dc30df73b1beb29596a375.cloudfront.net (CloudFront) x-amz-cf-pop: FRA53-C1 x-amz-cf-id: 2q5VsSKpUHNjHWKmeMQ5GIs1ue6pSubU680gneUqjP3oToG7Mu9-qw== 100 426k 100 426k 0 0 1068k 0 --:--:-- --:--:-- --:--:-- 1068k 21-04-21 19:41:13 % ll Hoya-king* -rw-r--r-- 1 maiito staff 436473 4 21 19:41 Hoya-king-2.png
指定した有効期限の1分を過ぎた後、改めて curl コマンドを実行してみました。結果は403エラー、アクセスに失敗します。-Oオプションによりローカルにファイルが生成されていますが、画像ファイル自体にはアクセスできていないため、中身は空(サイズが110B)、表示させることはできません。
21-04-21 19:47:22 % curl -D (中略) -O https://d1xxxxxxxxxxxx.cloudfront.net/Hoya-king-2.png % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0HTTP/2 403 server: CloudFront date: Wed, 21 Apr 2021 17:47:25 GMT content-type: text/xml content-length: 110 x-cache: Error from cloudfront via: 1.1 7549433a09d06354ea864d169b689e51.cloudfront.net (CloudFront) x-amz-cf-pop: FRA53-C1 x-amz-cf-id: gkUNC2uF0JMHsuchjsZHxOBUmNBoeXFYieD5544E8QZLBp1J9J5qMg== 100 110 100 110 0 0 940 0 --:--:-- --:--:-- --:--:-- 932 21-04-21 19:48:31 % ll Hoya-king* -rw-r--r-- 1 maiito staff 110 4 21 19:48 Hoya-king-2.png
以上です。試してみると理解が深まりますね!